객체 프로퍼티

객체 프로퍼티#

프로퍼티의 종류#

  1. 데이터 프로퍼티 (data property)
  2. 접근자 프로퍼티 (accessor property)
    • 본질은 함수
    • 값을 획득(get) / 설정(set)
    • 외부 코드에서는 함수가 아닌 일반적인 프로퍼티처럼 보인다.

둘 중 한 종류에만 속할 수 있다.

Object.defineProperties({}, 'prop', {
get() { // 접근자 프로퍼티의 설명자
return 1;
},
value: 2 // 데이터 프로퍼티의 설명자
}) // error

프로퍼티 특성 설명자 Property Descriptor#

ES5 이후, descriptor 값의 조회/수정이 가능해졌다.

프로퍼티 값과 플래그#

객체 프로퍼티: 값(value) + 플래그(flag) 속성 세 가지

  • value 프로퍼티 값 정의

    • type: 자바스크립트에서 허용한 모든 값
    • Default: undefined
  • writable

    • true 값을 수정할 수 있다.
  • enumerable

    • true 반복문을 사용해 나열 가능
  • configurable

    • true 프로퍼티 삭제/플래그 수정 가능

프로퍼티 플래그의 기본값은 모두 true

Object.getOwnPropertyDescriptor(obj, propertyName)#

  • 특정 프로퍼티에 대한 정보를 모두 반환
  • obj 정보를 얻고자 하는 객체
  • propertyName 정보를 얻고자 하는 객체 내 프로퍼티
let user = {
name: "John"
};
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
alert( JSON.stringify(descriptor, null, 2 ) );
/* property descriptor:
{
"value": "John",
"writable": true,
"enumerable": true,
"configurable": true
}
*/

Object.defineProperty(obj, propertyName, descriptor)#

  • 프로퍼티 플래그 변경

  • [obj, propertyName] = [설명자를 적용하고 싶은 객체, 객체 프로퍼티]

    • propertyName 이 없으면, 새로운 프로퍼티 생성
  • [descriptor] = [적용하고자 하는 프로퍼티 설명자]

  • 플래그 정보가 없으면 defaultfalse

let user = {};
Object.defineProperty(user, "name", {
value: "John"
});
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
"value": "John",
"writable": false,
"enumerable": false,
"configurable": false
}
*/

writable 플래그#

let user = {
name: "John"
};
Object.defineProperty(user, "name", {
writable: false
});
user.name = "Pete"; // Error: Cannot assign to read only property 'name'

비엄격 모드

  • 읽기 전용 프로퍼티 값 수정 -> 에러 X
  • BUT, 값 변경 X
  • 플래그에서 정한 규칙을 위반 -> 에러 없이 그냥 무시
let user = { };
Object.defineProperty(user, "name", {
value: "John",
// defineProperty를 사용해 새로운 프로퍼티를 만들 땐, 어떤 플래그를 true로 할지 명시해주어야 합니다.
enumerable: true,
configurable: true
});
alert(user.name); // John
user.name = "Pete"; // Error

enumerable 플래그#

객체 내장 메서드 toString

  • non-enumerable
  • for...in, Object.keys(), Object.entries(), Object.values() 에서 순회하지 않음.
  • custom toString 을 추가하면 for..in 에서 나타남
let user = {
name: "John",
toString() {
return this.name;
}
};
//커스텀 toString은 for...in을 사용해 열거할 수 있습니다.
for (let key in user) alert(key); // name, toString
Object.defineProperty(user, "toString", {
enumerable: false
});
// 이제 for...in을 사용해 toString을 열거할 수 없게 되었습니다.
for (let key in user) alert(key); // name
// 열거가 불가능한 프로퍼티는 Object.keys에도 배제됩니다.
alert(Object.keys(user)); // name

configurable 플래그#

false

  • 해당 프로퍼티는 제거/변경 X
  • configurable/enumerable 플래그 수정 X
  • writable
    • false -> true (X)
    • true -> false (O)
  • 접근자 프로퍼티 get/set 을 변경 X (새롭게 만드는 것은 가능)

Object.defineProperties(obj, descriptors)#

프로퍼티 여러개를 한 번에 정의할 수 있다.

Object.defineProperties(user, {
name: { value: "John", writable: false },
surname: { value: "Smith", writable: false },
// ...
});

Object.getOwnPropertyDescriptors#

프로퍼티 설명자를 전부 한꺼번에 가져올 수 있다.

  • Object.defineProperties 와 함께 사용하면
    • 플래그, 심볼형 프로퍼티도 함께 복사
let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));

객체 수정을 막아주는 다양한 메서드#

  • Object.preventExtensions(obj) 객체에 새로운 프로퍼티를 추가할 수 없게 합니다.
  • Object.seal(obj) 새로운 프로퍼티 추가나 기존 프로퍼티 삭제를 막아줍니다. 프로퍼티 전체에 configurable: false를 설정하는 것과 동일한 효과입니다.
  • Object.freeze(obj) 새로운 프로퍼티 추가나 기존 프로퍼티 삭제, 수정을 막아줍니다. 프로퍼티 전체에 configurable: false, writable: false를 설정하는 것과 동일한 효과입니다.

제약 사항을 확인하는 메서드

  • Object.isExtensible(obj) 새로운 프로퍼티를 추가하는 게 불가능한 경우 false를, 그렇지 않은 경우 true를 반환합니다.
  • Object.isSealed(obj) 프로퍼티 추가, 삭제가 불가능하고 모든 프로퍼티가 configurable: false이면 true를 반환합니다.
  • Object.isFrozen(obj) 프로퍼티 추가, 삭제, 변경이 불가능하고 모든 프로퍼티가 configurable: false, writable: false이면 true를 반환합니다.

접근자 프로퍼티 설명자 Accessor Descriptor#

  • value, writable 은 무시된다.
  • get 인수가 없는 함수, 프로퍼티를 읽을 때 동작
  • set 인수가 하나인 함수, 프로퍼티에 값을 쓸 때 호출됨
  • enumerable
  • configurable
let obj = {
get propName() {
// getter, obj.propName 을 실행할 때 실행된다.
},
set propName(value) {
// setter, obj.propName = value 를 실행할 때 실행되는 코드
}
}

바깥 코드에선 접근자 프로퍼티를 일반 프로퍼티처럼 사용할 수 있다.

let user = {
name: "John",
surname: "Smith",
get fullName() {
return `${this.name} ${this.surname}`;
}
};
alert(user.fullName);

프로퍼티에 getter 메서드만 있기 때문에 에러가 발생한다.

user.fullName = 'Test'; // Error

setter 추가

let user = {
// ...
set fullName(value) {
[this.name, this.surname] = value.split(" ");
}
};
user.fullName = "Alice Cooper";
console.log(user.name, user.surname); // Alice Cooper

fullName 은 가상의 프로퍼티 이다. 읽고 쓸 순 있지만 실존하지 않는다.

자바 getters, setters 의 용도#

🔗 🔗

  1. 유효성 검사
  2. Lazy Loading
  3. Read 와 Write 권한을 다르게 설정

유효성 검사#

클래스 외부에서 클래스의 private 필드 를 get/set 할때 사용한다.

  • 클래스 필드를 실수로 조작하지 못하도록 유효성 검사를 수행 할 수있는 중심 위치.

캡슐화#

동시성과 멀티스레딩 -> 불변객체 생성#

  • 객체의 상태의 불변성을 위해 setter 가 없다.
  • 객체를 동시에 실행중인 다른 스레드로 전달하려면, race condition 및 기타 멀티스레딩의 부작용을 피하기 위해 스레드들을 동기화 해야 한다.
  • 불변 객체의 경우 객체 상태가 스레드에 의해 변경될까봐 걱정할 필요가 없다.
  • 불변 객체 예제
public class MyObject {
// 모든 멤버 변수는 private - 캡슐화, final
private final int state;
private final String str;
// constructor(생성자) 는 변수를 정의 할 수있는 마지막 장소이다.
public MyObject(int state, String str) {
this.state = state;
this.str = str;
}
//Getters
public int getState() {
return this.state;
}
public String getStr() {
return this.str;
}
// 객체의 상태의 불변성을 위해 setter 가 없다.
}

getter 획득자 메서드#

보통, 계산된 값을 반환

Syntax#

  • 함수 이름
    • 자바스크립트 변수 이름 생성 규칙 (_$0-9, A-Z,a-z) 에 허용 되는 문자열.
  • 매개변수가 없다

선언#

var log = ['test'];
var obj = {
get latest () {
if (log.length == 0) return undefined;
return log[log.length - 1]
}
}

프로퍼티 접근을 해도, 정의된 함수가 호출되고 값이 반환된다.

console.log (obj.latest); // "test"를 반환.

삭제#

delete obj.latest

defineProperty, 객체에 새 프로퍼티로 추가#

const obj = { _name: 'DoonDoony' };
Object.defineProperty(obj, 'name', {
get: function() {
return `${this._name} the Cat!`;
},
enumerable: true,
configurable: true,
});
console.log(obj.name); // "DoonDoony the Cat!"

계산된 프로퍼티 이름#

const prefix = 'name';
const obj = {
get [prefix] () {
return 'Getter is called with prefix'
}
}

Immutable 패턴#

객체의 생명주기 동안 내부의 상태가 절대 변경되지 않도록 강제하는 방법

  • 객체의 프로퍼티 값 할당을 생성자를 통해서만 할 수 있다.
  • 필드에 접근하기 위해서는 Getter 메서드를 사용해야 한다.
const _list = new WeakMap();
const key = new Object();
class SomeClass {
constructor(list) {
_list.set(key, list);
}
// 필드 자체 값 대신에 복사본을 반환
getList() {
// 객체를 복제하는 clone()이라는 메서드가 있다고 가정
return _list.get(key).clone();
}
}

lazy evaluation#

getter 프로퍼티에 접근하기 전까지는 값을 계산하지 않는다.

  • 값의 계산 비용이 큰 경우
    • RAM 이나 CPU 의 시간을 많이 소모할 때
    • worker thread 생성 (?)
    • 원격 파일 로드
  • 값이 당장 필요하지 않을때, 나중에 이용될 때, 절대로 이용되지 않을 때
  • 값이 다시 계산되어서는 안되는 경우.
  • 값이 여러차례 이용되지만, 절대 변경되지 않아 매번 다시 계산할 필요가 없을 때

    각 케이스가 궁금하다 TODO

getter 반환값의 캐싱#

  • getter 은 첫 호출 이후에는 다시 계산하지 않고 이 캐시 값을 반환한다.

  • setter 에서 delete 를 하든 getter 에서 delete 를 하든, delete 를 호출하면 setter, getter 모두가 해제 된다.

const o = {
_foo: '',
set foo (val) {
delete this.foo;
this.foo = val;
},
get foo () {
delete this.foo;
return this.foo = 'something';
}
};
o.foo = "test";
console.log(o.foo); // 'test' 출력
  • o.foo = "test" 실행 -> getter 인 foo 삭제
  • console.log(o.foo) 실행
    • get foo() {} 를 실행 X
    • o.foo 프로퍼티의 값을 반환하여 "test" 를 출력 O

🔗 o.foo = "test" 이후의 setter 가 활성화 되고, delete this.foo 는 setter 를 삭제하여 다음줄의 this.foo 는 객체의 프로퍼티에 값을 할당하게 됩니다. 그러나 만약에 delete this.foo 가 없다면, this.foo는 다시 setter 를 호출하게 되므로 throws RangeError maximum call 를 유발합니다. 이것의 대안은 this._foo 처럼 프로퍼티 명을 private 형식으로 지정해 줄 수 있습니다.

var o = {
set foo (val) {
this._foo = val;
},
// ...
};

get foo() { delete this.foo } foo 프로퍼티를 삭제해도 결과는 test 을 출력한다. 왜 delete 를 적어준 것일까요?

  • 다시 o.foo 를 얻어 올때, getter 으로 값을 다시 계산하는 것을 없애기 위해서 입니다. 단순히 o의 프로퍼티인 foo 로서 값을 얻어 옵니다.
var o = {
_foo: '',
set foo (val) {
this._foo = val;
},
get foo () {
return this._foo = 'something';
}
};
  • return this._foo = 'something'; 문자열을 반환하는 이유
    - 자바스크립트에서 할당문은 할당한 값을 반환 (const, let, var 키워드 제외)
    이 예제는 getter 와 setter 를 캐싱으로서 사용하지 않을 때 입니다.
o.foo = "test"; //test
o.foo; // something

아무리 setter 를 해서 _foo 의 값을 변경 시켜도, o.foo 으로 getter 를 호출해도 항상 'something' 으로 됩니다. this._foo = 'something'에서 다시 setter 를 호출하기 때문입니다. 이 예제는 도대체 왜 만든건지 모르겠네요.

중요한 것은, 캐싱을 위해 의도적으로 getter 와 setter 를 사용할 수 있다는 점 같습니다.

setter 설정자 메서드#

const cat = {
_name: undefined,
set name(newName) {
this._name = newName;
},
call() {
console.log(this._name);
},
};
cat.name = 'DoonDoony';
cat.call(); // 'DoonDoony'

setter 에서 값을 할당하는 내부적 과정#

1. 접근하는 프로퍼티 탐색#

직속 프로퍼티

  1. 접근하는 프로퍼티가 Accessor Descriptor(Getter/Setter) 일 때

    • [Setter] 호출.
    • [Setter] 가 없고 [Getter]만 있을 때
      • 값 할당은 무시된다.
  2. 접근자 설명자가 아니고, writable: false 일 때

    • 조용히 실패
    • 엄격모드일 땐, Type Error 발생
  3. 1번 2번 모두 해당하지 않을때, 프로퍼티에 값을 세팅

  4. 접근하는 프로퍼티가 없을 때

    • 객체와 연결된 상위 [[Prototype]] 체인을 순회한다.

2. 모든 [[Prototype]] 체인에서 프로퍼티가 발견되지 않을 경우#


  1. Object extensible
    • 직속 프로퍼티(Directly Present) 생성하고 값을 할당
  2. not Object extensible
    • Object.preventExtensions(), Object.isExtensible()[false] 일 때,
      'use strict';
      const parentCat = {
      name: 'DoonDoon',
      age: 10,
      };
      const childCat = Object.create(parentCat);
      Object.assign(childCat, { name: 'DoonDoony', age: 3, favorite: 'Red Ball' });
      childCat 을 확장 불가능하게 만듭니다
      Object.preventExtensions(childCat);
      // childCat 이 확장 불가능한지 확인합니다
      console.log(Object.isExtensible(childCat)); // false
    • TypeError 발생
      childCat.gender = 'male'; // TypeError!
    • 프로퍼티의 삭제/수정 가능
      childCat.favorite = 'Box';
      console.log(childCat.favorite); // Box
      delete childCat.favorite; // true
      console.log(childCat.favorite); // undefined
    • 오브젝트의 proto 에 값 추가, 변경 가능 [[Prototype]] 에 새 프로퍼티를 추가합니다
      childCat.__proto__.gender = 'male';
      console.log(Object.getPrototypeOf(childCat)); // { name: 'DoonDoon', age: 10, gender: 'male' }

3. 상위 [[Prototype]] 체인에서 프로퍼티가 발견된 경우 🔗#

  1. writable: true

    • 직속 프로퍼티를 생성하고 값을 할당
      • 의도는 상위 수준의 프로퍼티에 할당을 하려고 했지만 직속 프로퍼티가 추가 된다.
      • 객체지향에서 Overriding 이라고 부르는 Shadowing 이 발생한다.
    let anotherObj = {
    a: 2,
    };
    let myObject = Object.create(anotherObj);
    console.log(anotherObj.a);
    console.log(myObject.a);
    console.log(anotherObj.hasOwnProperty('a')); //ture
    console.log(myObject.hasOwnProperty('a')); // false

    이것이 위임을 통한 프로퍼티 가려짐이다. anotherObj.a가 증가함이 아니라. myObject에 a가 새로 할당된다.

    console.log(myObject.a++); // 2;
    console.log(anotherObj.a); //2 ;
    console.log(myObject.a); // 3;
    console.log(myObject.hasOwnProperty('a')); // true
  2. writable: false

    • 아무일도 일어나지 않는다.
    • 엄격모드일 경우 TypeError

프로퍼티가 [Setter] 일 경우, 항상 Setter 가 호출된다.

  • 직속 프로퍼티가 추가 되지 않는다.
  • Setter 를 덮어 쓰려면 Object.defineProperty 를 사용해야 한다.

getter 와 setter 똑똑하게 활용하기#

1. 실제 프로퍼티 값을 감싸는 wrapper 처럼 사용#

프로퍼티 값을 원하는 대로 통제 가능

let user = {
get name () {
return this._name;
},
set name (value) {
if (value.length < 4) {
alert('입력값이 너무 짧습니다. 4자 이상으로 입력하세요.');
return;
}
this._name = value;
}
};
user.name = "Pete";
alert(user.name);
user.name = "";

user.name 으로 접근할 수 있지만. 밑줄로 시작하는 프로퍼티는 객체 내부에서만 활용하고, 외부에서는 건드리지 않는 것이 관습.

2. 호환성을 위해 사용하기#

데이터 프로퍼티 name, age 를 사용해서 사용자를 나타내는 객체 구현

function User(name, age) {
this.name = name;
this.age = age;
}
let john = new User("John", 25);
alert(john.age); // 25

요구사항이 바뀌어 age 를 birthday 로 저장해야 할 경우. (birthday 가 더 정확하고 편리하기 때문)

function User(name, birthday) {
this.name = name;
this.birthday = birthday;
}
let john = new User("John", new Date(1992, 6, 1));

기존 코드에 age 를 사용하고 있는 코드를 모두 찾아서 수정해야 하는 문제점이 있다.

function User(name, birthday) {
this.name = name;
this.birthday = birthday;
Object.defineProperties(this, 'age', {
get() {
let todayYear = new Date().getFullYear();
return todayYear - this.birthday.getFullYear();
}
});
}
let john = new User('John', new Date(1992, 6, 1));
alert(john.birthday);
alert(john.age);

Property - Existence#

객체에 프로퍼티가 존재하는지 여부를 확인하는 방법

const parent = { parentProp: 'Hey' };
const child = Object.create(parent);
child.childProp = 'Yay!';
  1. in 연산자
    • [[Prototype]] 체인을 모두 순회하며 키 존재 여부를 검사한다.
    console.log('parentProp' in child) // true
  2. Object.hasOwnProperty
    • 직속 프로퍼티 존재 여부만을 검사한다.
    console.log(child.hasOwnProperty('parentProp')); // false
    console.log(child.hasOwnProperty('childProp')); // true

Object.seal#

  • 더 이상 확장 불가능 하게 만듦(프로퍼티 추가가 불가능한 상태)
'use strict';
const obj = { a: 1, b: 2 };
Object.seal(obj);
try {
obj.c = 3;
} catch (e) {
// 확장 불가능 하기 때문에, TypeError!
console.error(e); // TypeError: Cannot add property c, object is not extensible
}
  • Object.defineProperty 로 Descriptor 를 변경하는 행위를 막음
try {
Object.defineProperty(obj, 'b', { enumerable: false });
} catch (e) {
console.error(e); // TypeError: Cannot redefine property: b
}
  • 프로퍼티 삭제 불가
try {
delete obj.a;
} catch (e) {
console.error(e); // TypeError: Cannot delete property 'a' of #<Object>
}

Object.freeze#

  • Object.seal 의 모든 동작을 포함
  • 프로퍼티 값 또한 변경 불가능 하게 만든다.
  • 중첩된 접근 값이 변경은 가능하다.
    • obj.a = { b: 1 } 이라면 obj.a 는 변경 불가 하지만, obj.a.b 는 변경 가능합니다.
Last updated on